﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Thor.Common;

namespace Thor.Domain
{
    /// <summary>
    /// Represents a calendar.
    /// </summary>
    public class Calendar : ThorDomainObjectBase, ICalendar
    {
        #region Constants

        private const string CALENDAREMAIL = "CalendarEmail";
        private const string APPOINTMENTS = "Appointments";
        private const string ALIAS = "Alias";
        private const string ID = "Id";
        private const string TIMEZONE = "TimeZone";
        private const string USEBOOKINGAGENT = "UseBookingAgent";
        private const string DISPLAYSUBJECT = "DisplaySubject";
        private const string PUBLICCANVIEW = "PublicCanView";
        private const string PUBLICCANBOOK = "PublicCanBook";
        private const string USETIMEFRAME = "UseTimeframe";
        private const string STARTTIME = "StartTime";
        private const string ENDTIME = "EndTime";

        #endregion

        #region Fields

        private string _calendarEmail = string.Empty;
        private List<Appointment> _appointments = null;
        private string _alias = string.Empty;
        private Guid _id = Guid.Empty;
        private string _timeZone = string.Empty;
        private bool _useBookingAgent = false;
        private bool _displaySubject = false;
        private bool _publicCanView = false;
        private bool _publicCanBook = false;
        private bool _useTimeFrame = false;
        private int? _startTime = null;
        private int? _endTime = null;
        private Calendar _cache = null;

        #endregion

        #region Constructors

        public Calendar() : base() { }

        #endregion

        #region Properties

        /// <summary>
        /// Unique id of the calendar.
        /// </summary>
        [Key]
        public Guid Id
        {
            get { return _id; }
            set
            {
                if (_id != value)
                {
                    _id = value;
                    OnPropertyChanged(ID);
                }
            }
        }

        /// <summary>
        /// The unique id of the calendar.
        /// </summary>
        [Required]
        public string CalendarEmail
        {
            get { return _calendarEmail; }
            set
            {
                if (_calendarEmail != value)
                {
                    _calendarEmail = value;
                    OnPropertyChanged(CALENDAREMAIL);
                }
            }
        }

        /// <summary>
        /// The alias (friendly name) of the calendar.
        /// </summary>
        [Required]
        public string Alias
        {
            get { return _alias; }
            set
            {
                if (_alias != value)
                {
                    _alias = value;
                    OnPropertyChanged(ALIAS);
                }
            }
        }

        /// <summary>
        /// The time zone the calendar is in.
        /// </summary>
        [Required]
        public string TimeZone
        {
            get { return _timeZone; }
            set
            {
                if (_timeZone != value)
                {
                    _timeZone = value;
                    OnPropertyChanged(TIMEZONE);
                }
            }
        }

        /// <summary>
        /// Whether or not to use a booking agent when booking appointments
        /// for the calendar.
        /// </summary>
        public bool UseBookingAgent
        {
            get { return _useBookingAgent; }
            set
            {
                if (_useBookingAgent != value)
                {
                    _useBookingAgent = value;
                    OnPropertyChanged(USEBOOKINGAGENT);
                }
            }
        }

        /// <summary>
        /// Whether or not to display the subject of the calendar's
        /// appointments.
        /// </summary>
        public bool DisplaySubject
        {
            get { return _displaySubject; }
            set
            {
                if (_displaySubject != value)
                {
                    _displaySubject = value;
                    OnPropertyChanged(DISPLAYSUBJECT);
                }
            }
        }

        /// <summary>
        /// Whether or not the unauthenticated users can view
        /// appointments on the calendar.
        /// </summary>
        public bool PublicCanView
        {
            get { return _publicCanView; }
            set
            {
                if (_publicCanView != value)
                {
                    _publicCanView = value;
                    OnPropertyChanged(PUBLICCANVIEW);
                }
            }
        }

        /// <summary>
        /// Whether or not unauthenticated users can book
        /// appointments on the calendar.
        /// </summary>
        public bool PublicCanBook
        {
            get { return _publicCanBook; }
            set
            {
                if (_publicCanBook != value)
                {
                    _publicCanBook = value;
                    OnPropertyChanged(PUBLICCANBOOK);
                }
            }
        }

        /// <summary>
        /// Whether or not to use a timeframe when displaying
        /// appointments.
        /// </summary>
        public bool UseTimeframe
        {
            get { return _useTimeFrame; }
            set
            {
                if (_useTimeFrame != value)
                {
                    _useTimeFrame = value;
                    OnPropertyChanged(USETIMEFRAME);
                }
            }
        }

        /// <summary>
        /// Start time of the timeframe.
        /// </summary>
        public int? StartTime
        {
            get { return _startTime; }
            set
            {
                if (_startTime != value)
                {
                    _startTime = value;
                    OnPropertyChanged(STARTTIME);

                    if (!_startTime.HasValue)
                        StartTimeDisplay = string.Empty;
                    else
                    {
                        var time = Time.GetTime(_startTime.Value);

                        if (time == null)
                            StartTimeDisplay = string.Empty;
                        else
                            StartTimeDisplay = time.Text;
                    }
                }
            }

        }

        /// <summary>
        /// End time of the timeframe.
        /// </summary>
        public int? EndTime
        {
            get { return _endTime; }
            set
            {
                if (_endTime != value)
                {
                    _endTime = value;
                    OnPropertyChanged(ENDTIME);

                    if (!_endTime.HasValue)
                        EndTimeDisplay = string.Empty;
                    else
                    {
                        var time = Time.GetTime(_endTime.Value);

                        if (time == null)
                            EndTimeDisplay = string.Empty;
                        else
                            EndTimeDisplay = time.Text;
                    }
                }
            }
        }

        public string StartTimeDisplay { get; set; }

        public string EndTimeDisplay { get; set; }

        public DateTime Now
        {
            get
            {
                var currentUtcTime = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Unspecified);
                var currentLocalTime = TimeZoneInfo.ConvertTimeFromUtc(currentUtcTime, GetTimeZoneInfo());
                return currentLocalTime;
            }
        }


        /// <summary>
        /// The appointments associated with the calendar.
        /// </summary>
        public List<Appointment> Appointments
        {
            get { return _appointments; }
            set
            {
                if (_appointments != value)
                {
                    _appointments = value;
                    OnPropertyChanged(APPOINTMENTS);
                }
            }
        }

        #endregion

        #region Methods

        public bool IsBusy(DateTime startTime, DateTime endTime)
        {
            if (this.Appointments == null)
                return false;

            var appointments = (from a in this.Appointments
                                where (a.StartTime >= startTime && a.StartTime <= endTime)
                                 || (a.StartTime < startTime && a.EndTime >= endTime)
                                select a).ToList().Count();

            return appointments > 0;
        }

        public Appointment GetAppointment(DateTime startTime, DateTime endTime)
        {
            if (this.Appointments == null)
                return null;

            var appointment = (from a in this.Appointments
                               where (a.StartTime >= startTime && a.StartTime <= endTime)
                                || (a.StartTime < startTime && a.EndTime >= endTime)
                               select a).FirstOrDefault();
            return appointment;
        }

        public bool IsOffHours(DateTime time)
        {
            if (time.Hour < 8 || time.Hour >= 17)
                return true;
            else
                return false;
        }

        public bool IsCurrentTime(DateTime startTime, DateTime endTime)
        {
            var currentUtcTime = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Unspecified);
            var currentLocalTime = TimeZoneInfo.ConvertTimeFromUtc(currentUtcTime, GetTimeZoneInfo());

            if (currentLocalTime >= startTime && currentLocalTime <= endTime)
            {
                Trace.TraceInformation(string.Format("Current Utc Time: {0}", currentUtcTime.ToString()));
                Trace.TraceInformation(string.Format("Current Local ({0}) time: {1}", this.TimeZone, currentLocalTime.ToString()));
                return true;
            }
            else
                return false;
        }

        private TimeZoneInfo GetTimeZoneInfo()
        {
            if (string.IsNullOrEmpty(TimeZone))
            {
                return TimeZoneInfo.Utc;
            }
            else
            {
                var timeZoneInfo = (from t in TimeZoneInfo.GetSystemTimeZones()
                                    where t.Id == TimeZone
                                    select t).FirstOrDefault();

                if (timeZoneInfo == null)
                    return TimeZoneInfo.Utc;
                else
                    return timeZoneInfo;
            }
        }

        public override void BeginEdit()
        {
            _cache = new Calendar();
            _cache.CalendarEmail = this.CalendarEmail;
            _cache.Alias = this.Alias;
            _cache.TimeZone = this.TimeZone;
            _cache.Appointments = this.Appointments;
            _cache.UseBookingAgent = this.UseBookingAgent;
            _cache.DisplaySubject = this.DisplaySubject;
            _cache.PublicCanBook = this.PublicCanBook;
            _cache.PublicCanView = this.PublicCanView;
            _cache.UseTimeframe = this.UseTimeframe;
            _cache.StartTime = this.StartTime;
            _cache.EndTime = this.EndTime;
        }

        public override void CancelEdit()
        {
            this.CalendarEmail = _cache.CalendarEmail;
            this.Alias = _cache.Alias;
            this.TimeZone = _cache.TimeZone;
            this.Appointments = _cache.Appointments;
            this.UseBookingAgent = _cache.UseBookingAgent;
            this.DisplaySubject = _cache.DisplaySubject;
            this.PublicCanBook = _cache.PublicCanBook;
            this.PublicCanView = _cache.PublicCanView;
            this.UseTimeframe = _cache.UseTimeframe;
            this.StartTime = _cache.StartTime;
            this.EndTime = _cache.EndTime;
        }

        public override void EndEdit()
        {
            _cache = null;
        }

        public override string ToString()
        {
            if (Appointments != null && Appointments.Count > 0)
            {
                StringBuilder sb = new StringBuilder();

                foreach (var appointment in Appointments)
                {
                    sb.AppendLine(appointment.ToString());
                }

                return sb.ToString();
            }
            else
                return "Calendar does not contain appointment data.";
        }

        public override bool Equals(object obj)
        {
            if (obj != null && obj is Calendar)
            {
                Calendar target = (Calendar)obj;

                if (this.Appointments == null && target.Appointments != null)
                {
                    return false;
                }
                else if (this.Appointments != null && target.Appointments == null)
                {
                    return false;
                }
                else
                {
                    if (this.Appointments.Count != target.Appointments.Count)
                    {
                        return false;
                    }
                    else
                    {
                        //TODO: need to consider multiple appointments with the same start/end time?
                        foreach (var sourceAppointment in this.Appointments)
                        {
                            var targetAppointment = (from appointment in target.Appointments
                                                     where appointment.StartTime == sourceAppointment.StartTime
                                                         && appointment.EndTime == sourceAppointment.EndTime
                                                     select appointment).FirstOrDefault();

                            if (targetAppointment == null)
                                return false;
                        }

                        foreach (var targetAppointment in this.Appointments)
                        {
                            var sourceAppointment = (from appointment in this.Appointments
                                                     where appointment.StartTime == targetAppointment.StartTime
                                                        && appointment.EndTime == targetAppointment.EndTime
                                                     select appointment).FirstOrDefault();

                            if (sourceAppointment == null)
                                return false;
                        }

                        return true;
                    }
                }
            }
            else
                return base.Equals(obj);
        }

        #endregion

        #region Time Conversion Methods

        private TimeZoneInfo GetTimeZoneInfoById(string timeZoneId)
        {
            if (string.IsNullOrEmpty(timeZoneId))
            {
                return TimeZoneInfo.Utc;
            }
            else
            {
                var timeZoneInfo = (from t in TimeZoneInfo.GetSystemTimeZones()
                                    where t.Id == timeZoneId
                                    select t).FirstOrDefault();

                if (timeZoneInfo == null)
                    return TimeZoneInfo.Utc;
                else
                    return timeZoneInfo;
            }
        }

        public void ConvertFromUtc()
        {
            if (Appointments != null && Appointments.Count > 0)
            {
                Trace.TraceInformation(string.Format("Looking up timezone for calendar {0}. Current specified as: {1}", CalendarEmail, TimeZone));

                var timeZoneInfo = GetTimeZoneInfoById(TimeZone);

                if (timeZoneInfo == TimeZoneInfo.Utc)
                {
                    Trace.TraceInformation(string.Format("Unable to find timzone {0} for calendar {1}. Using utc time.", TimeZone, CalendarEmail));
                    return;
                }

                Trace.TraceInformation(string.Format("Converting {0} appointments for calendar {1}.", Appointments.Count.ToString(), CalendarEmail));

                foreach (var appointment in Appointments)
                {
                    Trace.TraceInformation(string.Format("Converting apppointment {0} from utc to {1}", appointment.Subject, TimeZone));
                    Trace.TraceInformation(string.Format("Appointment start time: {0}", appointment.StartTime.ToString()));
                    Trace.TraceInformation(string.Format("Appointment end time: {0}", appointment.EndTime.ToString()));

                    DateTime utcStartTime = DateTime.SpecifyKind(appointment.StartTime, DateTimeKind.Unspecified);
                    DateTime utcEndTime = DateTime.SpecifyKind(appointment.EndTime, DateTimeKind.Unspecified);

                    Trace.TraceInformation(string.Format("Appointment utc start time: {0}", utcStartTime.ToString()));
                    Trace.TraceInformation(string.Format("Appointment utc end time: {0}", utcEndTime.ToString()));

                    appointment.StartTime = TimeZoneInfo.ConvertTimeFromUtc(utcStartTime, timeZoneInfo);
                    appointment.EndTime = TimeZoneInfo.ConvertTimeFromUtc(utcEndTime, timeZoneInfo);

                    Trace.TraceInformation(string.Format("Appointment local start time: {0}", appointment.StartTime.ToString()));
                    Trace.TraceInformation(string.Format("Appointment local end time: {0}", appointment.EndTime.ToString()));
                }
            }
        }

        public void ConvertToUtc()
        {
            if (Appointments != null && Appointments.Count > 0)
            {
                Trace.TraceInformation(string.Format("Looking up timezone for calendar {0}. Current specified as: {1}", CalendarEmail, TimeZone));

                var timeZoneInfo = GetTimeZoneInfoById(TimeZone);

                if (timeZoneInfo == TimeZoneInfo.Utc)
                {
                    Trace.TraceInformation(string.Format("Unable to find timzone {0} for calendar {1}. Using utc time.", TimeZone, CalendarEmail));
                    return;
                }

                Trace.TraceInformation(string.Format("Converting {0} appointments for calendar {1}.", Appointments.Count.ToString(), CalendarEmail));

                foreach (var appointment in Appointments)
                {
                    Trace.TraceInformation(string.Format("Converting apppointment {0} from {1} to utc", appointment.Subject, TimeZone));
                    Trace.TraceInformation(string.Format("Appointment start time: {0}", appointment.StartTime.ToString()));
                    Trace.TraceInformation(string.Format("Appointment end time: {0}", appointment.EndTime.ToString()));

                    DateTime localStartTime = DateTime.SpecifyKind(appointment.StartTime, DateTimeKind.Unspecified);
                    DateTime localEndTime = DateTime.SpecifyKind(appointment.EndTime, DateTimeKind.Unspecified);

                    Trace.TraceInformation(string.Format("Appointment local start time: {0}", localStartTime.ToString()));
                    Trace.TraceInformation(string.Format("Appointment local end time: {0}", localEndTime.ToString()));

                    appointment.StartTime = TimeZoneInfo.ConvertTimeToUtc(localStartTime, timeZoneInfo);
                    appointment.EndTime = TimeZoneInfo.ConvertTimeToUtc(localEndTime, timeZoneInfo);

                    Trace.TraceInformation(string.Format("Appointment utc start time: {0}", appointment.StartTime.ToString()));
                    Trace.TraceInformation(string.Format("Appointment utc end time: {0}", appointment.EndTime.ToString()));
                }
            }
        }

        #endregion
    }
}
